package com.yerbamateprimer.round.state.exit.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Preconditions;
import com.google.common.collect.Multimap;
import com.yerbamateprimer.action.system.SystemAction;
import com.yerbamateprimer.round.Round;
import com.yerbamateprimer.round.state.State;
import com.yerbamateprimer.round.state.StateName;
import com.yerbamateprimer.round.state.exit.StateExit;

/**
 * This class implements the 
 * common methods of a state 
 * exit. It's intended to use
 * as method template pattern.
 * 
 * @author Matias Liwski
 * @version 1.0
 */
public abstract class StateExitTemplate implements StateExit {
	private final static transient Logger LOGGER = LoggerFactory.getLogger(StateExitTemplate.class);
	private final static transient ScheduledExecutorService SCHEDULE_EXECUTOR = Executors.newScheduledThreadPool(5);
	private final static transient ExecutorService FIXED_EXECUTOR = Executors.newFixedThreadPool(5);

	/**
	 * @return The multimap of delay as key, system action as values for each class that override this class.
	 */
	protected abstract Multimap<Long, SystemAction> getActionsMap();

	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.exit.StateExit#addAction(com.yerbamateprimer.action.system.SystemAction)
	 */
	@Override
	final public void addAction(SystemAction action) {
		this.addAction(action, 0L);
	}

	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.exit.StateExit#addAction(com.yerbamateprimer.action.system.SystemAction, java.lang.Long)
	 */
	@Override
	final public void addAction(SystemAction action, Long delay) {
		Preconditions.checkArgument(action != null, "Couldn't add a null system action to this state.");
		Preconditions.checkArgument(delay != null && delay >= 0, "The delay has to be greater than zero.");
		getActionsMap().put(delay, action);
	}

	/**
	 * @param The delay to get the actions.
	 * @return A safe copy of the actions for the provided delay.
	 * 
	 * @see com.yerbamateprimer.round.state.exit.StateExit#getActions(java.lang.Long)
	 */
	@Override
	final public List<SystemAction> getActions(Long delay) {
		return new ArrayList<SystemAction>(getActionsMap().get(delay));
	}

	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.exit.StateExit#getActions()
	 */
	@Override
	final public Map<Long, Collection<SystemAction>> getActions() {
		return getActionsMap().asMap();
	}

	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.exit.StateExit#setNextStateName(com.yerbamateprimer.round.state.StateName)
	 */
	@Override
	public void setNextStateName(StateName nextStateName) {
		throw new IllegalStateException("The next state for this exit is fixed.");
	}

	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.exit.StateExit#apply(com.yerbamateprimer.round.Round)
	 */
	@Override
	final public void apply(Round round){
		Preconditions.checkArgument(round != null, "Couldn't apply this exit without a round");
		Preconditions.checkState(isThisExit(round.getCurrentState()), "Couldn't apply this exit if this isn't for the current state.");
		
		List<Long> delaysList = getSortedDelayList();
		executeImmediateActions(round);
		ScheduledFuture<?> lastSchedule = scheduleDelayedActions(round, delaysList);
		
		try {
			if(lastSchedule != null) {
				lastSchedule.get();
			}
		} catch (Exception exception) {
			LOGGER.warn("An exception happend when trying to wait the last scheduled exception.", exception);
			throw new RuntimeException(exception);
		}
		State nextState = round.getState(getNextStateName());
		nextState.getIn(round);
	}

	/**
	 * @return The sorted list of delays, without the immediate delay (0 millis).
	 */
	private List<Long> getSortedDelayList() {
		List<Long> delaysList = new ArrayList<Long>(getActions().keySet());
		Collections.sort(delaysList);
		delaysList.remove(0L);
		return delaysList;
	}

	/**
	 * Execute the system actions with a delay of zero seconds.
	 */
	private void executeImmediateActions(Round round) {
		for (SystemAction action : getActions(0L)) {
			LOGGER.debug("Executing immediately {} with {}.", action, round);
			FIXED_EXECUTOR.execute(new ThreadTask(action, round));
		}
	}

	/**
	 * @return The last schedule to join for.
	 */
	ScheduledFuture<?> scheduleDelayedActions(Round round, List<Long> delaysList) {
		ScheduledFuture<?> lastSchedule = null;
		for (Long delay : delaysList) {
			for (SystemAction action : getActions(delay)) {
				LOGGER.debug("Scheduling {} to run {} millis later.", action, delay);
				lastSchedule = SCHEDULE_EXECUTOR.schedule(new ThreadTask(action, round), delay, TimeUnit.MILLISECONDS);
			}
		}
		return lastSchedule;
	}

	/**
	 * Helper class to run the actions each one after his delay.
	 */
	private class ThreadTask implements Runnable {
		private final SystemAction action;
		private final Round round;
		
		public ThreadTask(SystemAction action, Round round) {
			super();
			this.action = action;
			this.round = round;
		}

		public void run() {
			LOGGER.debug("Executing " + action + " at " + round.getCurrentState() + " with " + round + ".");
			action.execute(round);
		}

		/* (non-Javadoc)
		 * @see java.lang.Object#toString()
		 */
		@Override
		public String toString() {
			return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE, false, true);
		}
	}
}